#include "SCC/parser/parser.h"

namespace scc::parser {

using namespace ast;
using namespace ast::common;
using namespace parser::common;

using std::format;

template<typename NodeType,
    typename std::enable_if<std::is_base_of<INode, NodeType>::value>::type* = nullptr>
using NodePtr = std::shared_ptr<NodeType>;

StmtType Parser::ParseDDLStatementType() {
  auto next_token = NextToken();
  ValidateIsWord(next_token);
  auto first_keyword_token = ASTUtils::CastToNodeType<StringNode>(next_token);
  std::string first_keyword = first_keyword_token->data;

  ValidateHasTokens(format("Missed keyword for \'{}\' DDL statement at line {}",
                           first_keyword, first_keyword_token->line));
  next_token = NextToken();
  ValidateIsWord(next_token);
  auto second_keyword_token = ASTUtils::CastToNodeType<StringNode>(next_token);
  std::string second_keyword = second_keyword_token->data;

  std::string ddl_stmt = first_keyword + " " + second_keyword;
  try {
    StmtType ddl_stmt_type(ddl_stmt);
    return ddl_stmt_type;
  } catch (const std::invalid_argument& ia) {
    throw parsing_error(format("Unsupported DDL Statement \'{}\' at line {}",
                               ddl_stmt, first_keyword_token->line));
  }
}
NodePtr<INode> Parser::ParseDDLStatement() {
  auto peeked_token = PeekToken();
  NodePtr<INode> service_node = ASTUtils::CreateServiceNode(StmtType::kDdlStmt, peeked_token);

  StmtType ddl_stmt_type = ParseDDLStatementType();
  ValidateHasTokens(format("Missed body for \'{}\' statement at line {}",
                           scc::common::UpperCase(ddl_stmt_type.ToString()), peeked_token->line));
  NodePtr<INode> statement;
  switch (ddl_stmt_type) {
    case StmtType::kCreateDatabaseStmt:
      statement = ParseCreateDatabaseStatement();
      break;
    case StmtType::kCreateTableStmt:
      statement = ParseCreateTableStatement();
      break;
    case StmtType::kAlterTableStmt:
      statement = ParseAlterTableStatement();
      break;
    case StmtType::kDropDatabaseStmt:
      statement = ParseDropDatabaseStatement();
      break;
    case StmtType::kDropTableStmt:
      statement = ParseDropTableStatement();
      break;
    default:
      throw parsing_error(format("Unknown DDL Statement at line {}", peeked_token->line));
  }
  ASTUtils::Link(service_node, statement);

  return service_node;
}

NodePtr<INode> Parser::ParseCreateDatabaseStatement() {
  NodePtr<INode> service_node = ASTUtils::CreateServiceNode(StmtType::kCreateDatabaseStmt);
  NodePtr<INode> database_name = ParseDatabaseName();
  ASTUtils::Link(service_node, database_name);
  return service_node;
}
NodePtr<INode> Parser::ParseCreateTableStatement() {
  NodePtr<INode> service_node = ASTUtils::CreateServiceNode(StmtType::kCreateTableStmt);

  auto peeked_token = PeekToken();
  NodePtr<INode> table_name = ParseTableName();
  ASTUtils::Link(service_node, table_name);

  ValidateHasTokens(format("Missed table definition at line {}", peeked_token->line));
  NodePtr<INode> table_definition = ParseTableDefinition();
  ASTUtils::Link(service_node, table_definition);

  return service_node;
}
NodePtr<INode> Parser::ParseAlterTableStatement() {
  NodePtr<INode> service_node = ASTUtils::CreateServiceNode(StmtType::kAlterTableStmt);

  int line = PeekToken()->line;
  NodePtr<INode> table_name = ParseName();
  ASTUtils::Link(service_node, table_name);

  std::string msg_prefix = "Incorrect \'ALTER TABLE\' definition at line";

  ValidateHasTokens(format(R"({} {}: expected 'ADD' or 'DROP' keyword)", msg_prefix, line));
  auto next_token = NextToken();
  ValidateIsWord(next_token);
  std::string action_keyword = ASTUtils::CastToNodeType<StringNode>(next_token)->data;

  ValidateHasTokens(format("{} {}: expected \'{}\' definition",
                           msg_prefix , next_token->line, action_keyword));
  NodePtr<INode> argument;
  StmtType action_type = DetermineAlterTableActionType(action_keyword);
  switch (action_type) {
    case StmtType::kAddKW:
      argument = ParseAlterAddListDefinition();
      break;
    case StmtType::kDropKW:
      argument = ParseAlterDropListDefinition();
      break;
    default:
      line = PeekToken()->line;
      throw parsing_error(format("{} {}: unknown action \'{}\'",
                                 msg_prefix, line, action_keyword));
  }
  NodePtr<INode> action = ASTUtils::CreateServiceNode(action_type);
  ASTUtils::Link(action, argument);
  ASTUtils::Link(service_node, action);

  return service_node;
}
NodePtr<INode> Parser::ParseDropDatabaseStatement() {
  NodePtr<INode> service_node = ASTUtils::CreateServiceNode(StmtType::kDropDatabaseStmt);

  while (HasTokens()) {
    NodePtr<INode> database_name = ParseDatabaseName();
    ASTUtils::Link(service_node, database_name);

    if (HasTokens() && NodeDataClassifier::IsComma(PeekToken())) {
      auto next_token = NextToken();
      ValidateHasTokens(format("Invalid \'DROP DATABASE\' statement at line {}"
                               ": expected database name after comma", next_token->line));
      ValidateIsWord(PeekToken());
    } else {
      break;
    }
  }

  return service_node;
}
NodePtr<INode> Parser::ParseDropTableStatement() {
  NodePtr<INode> service_node = ASTUtils::CreateServiceNode(StmtType::kDropTableStmt);

  while (HasTokens()) {
    NodePtr<INode> table_name = ParseTableName();
    ASTUtils::Link(service_node, table_name);

    if (HasTokens() && NodeDataClassifier::IsComma(PeekToken())) {
      auto next_token = NextToken();
      ValidateHasTokens(format("Invalid \'DROP TABLE\' statement at line {}"
                               ": expected table name after comma", next_token->line));
      ValidateIsWord(PeekToken());
    } else {
      break;
    }
  }

  return service_node;
}

NodePtr<INode> Parser::ParseTableDefinition() {
  auto next_token = NextToken();
  ValidateIsOpeningRoundBracket(next_token);

  NodePtr<INode> table_definition = ASTUtils::CreateServiceNode(StmtType::kTableDef);

  while (HasTokens()) {
    NodePtr<INode> table_definition_element = ParseTableDefinitionElement();
    ASTUtils::Link(table_definition, table_definition_element);

    if (HasTokens() && NodeDataClassifier::IsComma(PeekToken())) {
      next_token = NextToken();
      ValidateHasTokens(format("Invalid table definition at line {}"
                               ": expected column or constraint definition", next_token->line));
      ValidateIsWord(PeekToken());
    } else {
      break;
    }
  }

  ValidateHasTokens(format("Missing closing round bracket at line {}", next_token->line));
  ValidateIsClosingRoundBracket(NextToken());

  return table_definition;
}
NodePtr<INode> Parser::ParseTableDefinitionElement() {
  auto peeked_token = PeekToken();
  StmtType stmt_type = ParseTableDefinitionElementType();
  switch (stmt_type) {
    case StmtType::kColumnDef:
      return ParseColumnDefinition();
    case StmtType::kConstraintKW:
    case StmtType::kPrimaryKey:
    case StmtType::kForeignKey:
      return ParseTableConstraint(stmt_type);
    default:
      throw parsing_error(format("Unknown element in table definition at line {}",
                                 peeked_token->line));
  }
}
StmtType Parser::ParseTableDefinitionElementType() {
  ValidateIsWord(PeekToken());
  std::string next_word = ASTUtils::CastToNodeType<StringNode>(PeekToken())->data;
  if (DetermineIsFullConstraintDefinition(next_word)) {
    return StmtType::kConstraintKW;
  }
  StmtType constraint_type = DetermineConstraintType(next_word);
  if (constraint_type != StmtType::kNone) {
    return constraint_type;
  }
  return StmtType::kColumnDef;
}

NodePtr<INode> Parser::ParseColumnDefinition() {
  NodePtr<INode> service_node = ASTUtils::CreateServiceNode(StmtType::kColumnDef);

  int line = PeekToken()->line;
  NodePtr<INode> column_name = ParseColumnName();
  ASTUtils::Link(service_node, column_name);

  ValidateHasTokens(format("Missing column datatype at line {}" , line));
  NodePtr<INode> datatype = ParseDataType();
  ASTUtils::Link(service_node, datatype);

  return service_node;
}
NodePtr<INode> Parser::ParseTableConstraint(StmtType stmt_type) {
  NodePtr<INode> service_node = ASTUtils::CreateServiceNode(StmtType::kTableConstraint);

  int line = PeekToken()->line;
  if (stmt_type == StmtType::kConstraintKW) {
    NodePtr<INode> constraint_keyword =
        ASTUtils::CreateServiceNode(StmtType::kConstraintKW, NextToken());
    ASTUtils::Link(service_node, constraint_keyword);

    ValidateHasTokens(format("Missing constraint name at line {}", constraint_keyword->line));
    ValidateIsWord(PeekToken());
    NodePtr<INode> constraint_name = ParseConstraintName();
    line = constraint_name->line;
    ASTUtils::Link(constraint_keyword, constraint_name);
  }

  ValidateHasTokens(format("Missing constraint definition at line {}" , line));
  auto peeked_token = PeekToken();
  StmtType constraint_type;
  if (stmt_type != StmtType::kNone && stmt_type != StmtType::kConstraintKW) {
    constraint_type = stmt_type;
  } else {
    ValidateIsWord(peeked_token);
    std::string constraint_type_keyword = ASTUtils::CastToNodeType<StringNode>(peeked_token)->data;
    constraint_type = DetermineConstraintType(constraint_type_keyword);
  }

  NodePtr<INode> constraint_definition;
  switch (constraint_type) {
    case StmtType::kPrimaryKey:
      constraint_definition = ParsePrimaryKey();
      break;
    case StmtType::kForeignKey:
      constraint_definition = ParseForeignKey();
      break;
    default:
      throw parsing_error(format("Unknown constraint type at line {}" , peeked_token->line));
  }
  ASTUtils::Link(service_node, constraint_definition);

  return service_node;
}

NodePtr<INode> Parser::ParseAlterAddListDefinition() {
  NodePtr<INode> service_node = ASTUtils::CreateServiceNode(StmtType::kAlterAddList);

  while (HasTokens()) {
    NodePtr<INode> add_element = ParseAlterAddElement();
    ASTUtils::Link(service_node, add_element);

    if (HasTokens() && NodeDataClassifier::IsComma(PeekToken())) {
      auto next_token = NextToken();
      ValidateHasTokens(format("Missing \'ALTER TABLE ... ADD\' element in list at line {}",
                               next_token->line));
      ValidateIsWord(PeekToken());
    } else {
      break;
    }
  }

  return service_node;
}
NodePtr<INode> Parser::ParseAlterAddElement() {
  return ParseTableDefinitionElement();
}
NodePtr<INode> Parser::ParseAlterDropListDefinition() {
  NodePtr<INode> service_node = ASTUtils::CreateServiceNode(StmtType::kAlterDropList);

  while (HasTokens()) {
    NodePtr<INode> drop_element = ParseAlterDropElement();
    ASTUtils::Link(service_node, drop_element);

    if (HasTokens() && NodeDataClassifier::IsComma(PeekToken())) {
      auto next_token = NextToken();
      ValidateHasTokens(format("Missing \'ALTER TABLE ... DROP\' element in list at line {}",
                               next_token->line));
      ValidateIsWord(PeekToken());
    } else {
      break;
    }
  }

  return service_node;
}
NodePtr<INode> Parser::ParseAlterDropElement() {
  auto next_token = NextToken();
  std::string keyword = ASTUtils::CastToNodeType<StringNode>(next_token)->data;
  StmtType drop_element_type = DetermineDropElementType(keyword);
  NodePtr<INode> drop_element = ASTUtils::CreateServiceNode(drop_element_type);

  ValidateHasTokens(format("Missing \'ALTER TABLE ... DROP\' element definition at line {}",
                           next_token->line));
  auto peeked_token = PeekToken();
  ValidateIsWord(peeked_token);
  switch (drop_element_type) {
    case StmtType::kDropColumn:
      ParseAlterDropColumns(drop_element);
      break;
    case StmtType::kDropConstraint:
      ParseAlterDropConstraints(drop_element);
      break;
    default:
      throw parsing_error(format("Unknown \'ALTER TABLE ... DROP\' element type at line {}",
                                 peeked_token->line));
  }

  return drop_element;
}
void Parser::ParseAlterDropColumns(NodePtr<INode>& parent) {
  auto has_next_column_name_node = [this]() {
    if (HasTokens(2) && NodeDataClassifier::IsComma(PeekToken())) {
      auto peeked_token = PeekToken(1);
      if (NodeDataTypeClassifier::IsWord(peeked_token)) {
        std::string checking_word = ASTUtils::CastToNodeType<StringNode>(peeked_token)->data;
        return DetermineDropElementType(checking_word) == StmtType::kNone;
      }
    }
    return false;
  };

  while (HasTokens()) {
    auto column_name_node = ParseColumnName();
    ASTUtils::Link(parent, column_name_node);

    if (!has_next_column_name_node()) {
      break;
    }

    auto next_token = NextToken();
    ValidateHasTokens(format("Missing column name after comma at line {}", next_token->line));
    ValidateIsWord(PeekToken());
  }
}
void Parser::ParseAlterDropConstraints(NodePtr<INode>& parent) {
  auto has_next_constraint_name_node = [this]() {
    if (HasTokens(2) && NodeDataClassifier::IsComma(PeekToken())) {
      auto peeked_token = PeekToken(1);
      if (NodeDataTypeClassifier::IsWord(peeked_token)) {
        std::string checking_word = ASTUtils::CastToNodeType<StringNode>(peeked_token)->data;
        return DetermineDropElementType(checking_word) == StmtType::kNone;
      }
    }
    return false;
  };

  while (HasTokens()) {
    auto constraint_name_node = ParseConstraintName();
    ASTUtils::Link(parent, constraint_name_node);

    if (!has_next_constraint_name_node()) {
      break;
    }

    auto next_token = NextToken();
    ValidateHasTokens(format("Missing column name after comma at line {}", next_token->line));
    ValidateIsWord(PeekToken());
  }
}

} // scc::parser
